#####################################################################
# Product:      KnowledgeEngine (Plone Edition)
#
# File:         GenericObjectChooserEditlet.py
#
# Copyright:    Copyright (c) 2005, Faulkner Technologies
#
# Author:       Brian Oliver
#
# Description:  This file defines the GenericObjectChooserEditlet class, 
#               that is used allow the selection of generic objects 
#               within and including a model.
#
#               In addition to the standard PropertyDefinition attributes,
#               the following specialised functions are used to determine
#               rendering state;
#
#               isInitiallyExpandedFunction: function :: genericObject -> Boolean
#               isInitiallySelectedFunction: function :: genericObject -> Boolean
#               isDisplayableFunction: function :: genericObject -> Boolean
#               isSelectableFunction: function :: genericObject -> Boolean
#
# See Also:     PropertyEditlet, PropertyRefleciton, 
#               PropertyDefinition, PropertyAccess 
#####################################################################

#####################################################################
## KnowledgeEngine Library Imports
from Products.KnowledgeEngine.Common import *
from Products.KnowledgeEngine.Core import *
from Products.KnowledgeEngine.Editlet import Editlet
from Products.KnowledgeEngine.PropertyDefinition import *
from Products.KnowledgeEngine.PropertyTypes import *
from Products.KnowledgeEngine.PropertyFormats import *
from Products.KnowledgeEngine.PropertyStructures import *

#####################################################################
## Zope Library Imports
# (none)
 
#####################################################################
## Python Library Imports
# (none)

#####################################################################
## GenericObjectChooserEditlet Class Definition
class GenericObjectChooserEditlet(Editlet):
    """
        An editlet for editing enumerated types
    """
    
    #---------------------------------------------------------------#
    
    def isEditable(self, propertyDefinition, propertyAccess=None, value=None, isForConstructor=false, REQUEST=None, context=None):
        """
            Returns if the specified propertyDefinition can be
            edited by this editlet.
            
            For GenericObjectChoosers, we must have a model!
        """

        #we can only edit if we are for a chooser
        return propertyDefinition.getFormat() == pf_GENERIC_OBJECT_CHOOSER
        
    #---------------------------------------------------------------#

    def buildTree(self, propertyDefinition, selectedGenericObjectIdentities, genericObject, indentLevel):
        """
            PRIVATE: Constructs and returns HTML for the tree rooted at 
            the specified genericObject, indenting the generated HTML
            by the specified number of tabs.
            
            indentLevel of 0 is the first/root level ;)
        """

        #the default result
        result = ""
        
        #get the isDisplayableFunction to determine if the component should be displayed at all
        isDisplayable = propertyDefinition.getArg("isDisplayableFunction", None)
        
        #get the isInitiallySelectedFunction to determine if a component should be initially selected
        isInitiallySelectedFunction = propertyDefinition.getArg("isInitiallySelectedFunction", None)
        
        #get the isInitiallyExpandedFunction to determine if a composite component with children should initially be expanded
        isInitiallyExpandedFunction = propertyDefinition.getArg("isInitiallyExpandedFunction", None)
        
        nodeTitleFunction = propertyDefinition.getArg('nodeTitleFunction')
        
        onClickEvent = propertyDefinition.getArg('onClickEvent', '')
        
        model = propertyDefinition.getArg('getModelFunction')
        if type(model) is list:
            models = model
        else:
            models = [model]
        
        #is there a component and can we display it?
        if genericObject is not None and ((isDisplayable is not None and isDisplayable(genericObject)) or isDisplayable is None):
            #determine the indent level
            indent = ""
            for n in range(1, indentLevel):
                indent = indent + "    "

            #determine the unique object identifier for nodes and selections
            nodeId = "node%s%s" % (propertyDefinition.getIdentity(), genericObject.getIdentity())
            selectionId = "%s%s" % (propertyDefinition.getIdentity(), genericObject.getIdentity())

            #determine the node title
            if nodeTitleFunction:
                nodeTitle = nodeTitleFunction( genericObject );
            else:                
                nodeTitle = genericObject.getTitleOrId()

            #determine the node icon url
            nodeIconURL = genericObject.getIcon()

            singleSelection = propertyDefinition.getArg('singleSelection', false)
            if singleSelection:
                inputType = "radio"
            else:
                inputType = "checkbox"

            #generate the selection html for the component
            isSelectable = propertyDefinition.getArg("isSelectableFunction", None)
            if (isSelectable is not None and isSelectable(genericObject)) or isSelectable is None:
                #determine if selected (TODO: or selected by parent if parent selection implies child selection)
                if (selectedGenericObjectIdentities is not None and genericObject.getIdentity() in selectedGenericObjectIdentities) or \
                   (isInitiallySelectedFunction is not None and isInitiallySelectedFunction(genericObject)):
                    selected = " checked"
                else:
                    selected = ""

                #determine if a container component is selectable (it should select all of it's children ;)
                if genericObject.isContainer() and propertyDefinition.getArg("containerSelectionSelectsAll", false):
                    onClickEvent += ";changeState('%s', this.checked)" % (nodeId,)                    
                
                if singleSelection:
                    onClickEvent += ";resetAll(this)"
                    
                selectionContent = """<input type="%s" id="%s" name="%s" value="%s"%s onclick="%s" />""" % (inputType, selectionId, selectionId, genericObject.getIdentity(), selected, onClickEvent)
            else:
                selectionContent = """<input type="%s" id="%s" name="%s" disabled />""" % (inputType, selectionId, selectionId,)
                
            #generate the childContent
            childContent = ""
            if genericObject.isContainer() and genericObject.containsChildren():            
                #construct all of the child content... there may be none due to filtering!
                for child in genericObject.getChildren():
                    childContent = childContent + self.buildTree(propertyDefinition, selectedGenericObjectIdentities, child, indentLevel+1)            
            else:
                #there is no child content if there are no children!
                childContent == ""
            
            #display as expandable/collapseable if there is child content
            if len(childContent) > 0:
                #are we expanding all?
                expandAll = propertyDefinition.getArg("expandAll", false)
            
                #determine if the current component is the only toplevel component... if it is we autoexpand it
                isOnlyTopLevel = indentLevel == 0 and not genericObject.hasNextSibling() and not genericObject.hasPreviousSibling() and len(models) < 1
                
                #determine if we need to expand the node
                isExpanded = isOnlyTopLevel or expandAll or (isInitiallyExpandedFunction is not None and isInitiallyExpandedFunction(genericObject))
                if isExpanded:
                    expandImageName = "GenericObjectChooserEditlet_collapse.gif"
                else:
                    expandImageName = "GenericObjectChooserEditlet_expand.gif"

                result = result + indent + """<div>\n"""
                result = result + indent + """    <img class="branch" src="%s" border="0" id="branch%s" onClick="showBranch('leaf%s');swapImage('branch%s')">\n""" % (expandImageName, nodeId, nodeId, nodeId)
                result = result + indent + """    %s\n""" % (selectionContent,)
                result = result + indent + """    <span class="branch" onClick="showBranch('leaf%s');swapImage('branch%s')"><img src="%s" align="middle"/> %s</span>\n""" % (nodeId, nodeId, nodeIconURL, nodeTitle)
                result = result + indent + """</div>\n"""               

                if isExpanded:
                    result = result + indent + """<span class="leaf" style="display:block" id="leaf%s">\n""" % (nodeId,)
                else:
                    result = result + indent + """<span class="leaf" id="leaf%s">\n""" % (nodeId,)

                #add the child content ;)
                result = result + childContent

                result = result + indent + """</span>\n"""

            else:
                #no child content
                result = result + indent + """<div class="leaf" style="display:block">&nbsp;%s <img src="%s" align="middle"/> %s</div>\n""" % (selectionContent, nodeIconURL, nodeTitle)            

        #we're done here!
        return result
            
    #---------------------------------------------------------------#

    def generateEditorFor(self, propertyDefinition, propertyAccess=None, value=None, isForConstructor=false, REQUEST=None, context=None):
        """
            Returns a HTML editlet for the specified property.
            
            By default we use the provided context to determine the Model to choose.  If that is
            None we attempt to use the propertyAccess
        """
    
        #determine the model (so we can find components)       
        getModelFunction = propertyDefinition.getArg('getModelFunction', None)        
        if getModelFunction:
            model = getModelFunction(context)
        else:
            try:
                if context is None:
                    model = None
                elif context.isInstanceOf("Model"):
                    model = context
                else:
                    model = context.getModel()           
            except:
                try:
                    model = propertyAccess.getModel()
                except:
                    pass                

        #ensure that we use a writeable editlet (if possible) if we are dealing with a mandatory property and we don't yet have a value
        if propertyDefinition.isMandatory() and propertyAccess is not None and (not hasattr(propertyAccess, propertyDefinition.getIdentity()) or getattr(propertyAccess, propertyDefinition.getIdentity()) is None):
            
            #we want an editletEVAP with the most permissions (which may not include write access)
            editletEVAP = propertyDefinition.getConstructorEVAP() | propertyDefinition.getPropertyEditorEVAP()
        
        else:
            #determine the type of editlet we need to produce based on editlet value access permission (EVAM)
            editletEVAP = iif(isForConstructor, propertyDefinition.getConstructorEVAP(), propertyDefinition.getPropertyEditorEVAP())
                
        #-----------------
        #no property editor is required
        if editletEVAP == evap_NONE or model is None:
            result = ""

        #-----------------
        #a read-only property editor is required
        elif editletEVAP == evap_READONLY:

            #get the value for the property
            if isForConstructor or propertyAccess is None:
                value = propertyDefinition.getDefaultValue()
            else:
                value = propertyAccess.getPropertyValue(propertyDefinition.getIdentity())
            
            #TODO: create the editor by listing each of the component titles
            result = "*create readonly generic object editlet here for %s" % (value, )
        
        #-----------------
        #a writeable property editor is required
        else: 
       
            #get the value for the property (if possible)
            if editletEVAP == evap_WRITEONLY:
                value = None
            else:
                if isForConstructor or propertyAccess is None:
                    value = propertyDefinition.getDefaultValue()
                else:
                    #get the value for the property
                    value = propertyAccess.getPropertyValue(propertyDefinition.getIdentity())

            #the header
            result = """
                         <div class="root" style='border:solid 1px gray; width:%s; height:%s; overflow:scroll;' id='%s'>
                     """ % (propertyDefinition.getArg('width', '500px'), propertyDefinition.getArg('height', '400px'), propertyDefinition.getIdentity())
                     
            #determine the default object selections
            selectedGenericObjectIdentities = None
            if propertyAccess is not None:
                selectedGenericObjectIdentities = propertyAccess.getPropertyValue(propertyDefinition.getIdentity())
            if selectedGenericObjectIdentities is None:
                selectedGenericObjectIdentities = value
            if selectedGenericObjectIdentities is None:
                selectedGenericObjectIdentities = propertyDefinition.getDefaultValue()
            
            if not type(model) in (list, tuple):
                model = [model]
                
            for m in model:
                #recursively build the tree
                result = result + self.buildTree(propertyDefinition, selectedGenericObjectIdentities, m, 0)

            #the footer
            result = result + "</div>\n"
        
        return result
        
    #---------------------------------------------------------------#

    def containsPropertyValue(self, propertyDefinition, REQUEST):
        """
            Determines if the provided request contains
            a value for the specified propertyDefinition.
            
            This method is used (and is useful for) determining and
            updating the values of properties from property editors.
            
            All editlet implementations must override this method.            
        """
        
        #generic object choosers are always present (as no selection means there are no values)
        return true
        
    #---------------------------------------------------------------#

    def updatePropertyWith(self, propertyDefinition, propertyAccess, REQUEST=None):
        """
            Attempts to update the specified property on the
            object implementing the PropertyAccess interface with the
            specified HTTP REQUEST.
        """

        #the default result is nothing selected
        result = []

        #the prefix of the selections for the property
        selectionPrefix = "%s" % (propertyDefinition.getIdentity(), )

        #find the selections for the propertyDefinition and add them to the result
        for key in REQUEST.keys():
            #is the key for this property?
            if key.startswith(selectionPrefix):
                #determine the generic object identity and add it to the result
                genericObjectIdentity = key[len(selectionPrefix):]

                #add the object to the result!
                result = result + [genericObjectIdentity]

        # Check we have a value if it isMandatory
        propertyDefinition.verifyMandatoryValue(result)

        #set the value for the property
        propertyAccess.setPropertyValue(propertyDefinition.getIdentity(), result)
        
    #---------------------------------------------------------------#

    def generateEditorHTMLInitialisation(self, propertyDefinition, propertyAccess=None, value=None, isForConstructor=false, REQUEST=None, context=None):
        """
            Returns a string containing html that is placed at the top of the page.
            
            PRECONDITION:           isEditable(propertyDefinition, propertyAccess) 

            PARAMETERS:
            
            propertyDefinition:     An object implementing the PropertyDefinition
                                    interface.  This object is used to provide
                                    and determine property syntactic and 
                                    semantic information.  

            propertyAccess:         (optional) An object implement the PropertyAccess
                                    interface.  This object is used to 
                                    access the desired property value.  Generally
                                    a GenericObject descendant should be specifed
                                    here, although None is acceptable. If None is specified
                                    the value parameter is used as the default editing value.
                                    
                                    (remember: GenericObject implements the 
                                    PropertyAccess interface)

            value:                  (optional) A value to use if the propertyAccess 
                                    object is not provided.
                                    
            isForConstructor        (Optional) Is the Editlet for the an
                                    object constructor.  Default is false.

            REQUEST                 (Optional) The Request Object from Zope, providing information
                                    about the context in which the editlet is required.  Default is None.

            context                 (Optional) Usually the parent of the object being edited. Default is None.

            RETURN TYPE: String (of HTML)
        """

        return """
<style>
    .branch{
        cursor: pointer;
        cursor: hand;
    }
    .leaf{
        display: none;
        margin-left: 16px;
    }
</style>
<script language='javascript'>
    var collapseImg = new Image();
    collapseImg.src = "GenericObjectChooserEditlet_collapse.gif";
    var expandImg = new Image();
    expandImg.src = "GenericObjectChooserEditlet_expand.gif";

    function showBranch(leaf) {
        var objBranch = document.getElementById(leaf).style;
        if(objBranch.display=="block")
            objBranch.display="none";
        else
            objBranch.display="block";
    }

    function swapImage(img) {
        var objImg = document.getElementById(img);
        if(objImg.src.indexOf('GenericObjectChooserEditlet_expand.gif')>-1)
            objImg.src = collapseImg.src;
        else
            objImg.src = expandImg.src;
    }
    
    function getParentByClassName(node, className, safetyBelt) {
        if( safetyBelt == undefined ) safetyBelt=20;
        if(safetyBelt < 0) return null;
        
        if( node.className == className )
            return node;
        else
            return getParentByClassName( node.parentNode, className, safetyBelt-1 );
    }
    
    function resetAll(sender) {
        var root = getParentByClassName( sender, "root" );
        var inputs = root.getElementsByTagName('input');
        
        var x;
        for(x=0; x<inputs.length; x++)
        {
            var input = inputs[x];
            if( (input.type == 'checkbox' || input.type == 'radio') && input != sender )
                input.checked = false;
        }
    }
    
    var singleSelection = true;
    function changeState(nodeId, checked) {
        var node = document.getElementById("leaf" + nodeId);        
        var inputNodes = node.getElementsByTagName("input");
        for (var i = 0; i < inputNodes.length; i++) {
            inputNode = inputNodes[i];
            if (inputNode.type=="checkbox" && !inputNode.disabled) {
                inputNode.checked = checked;
            }
        }        
    }
</script>
              """
        
    #---------------------------------------------------------------#

#####################################################################
## Class Initialisation

# Register Editlet Information with the Knowledge Engine
registerEditlet("GenericObjectChooserEditlet", GenericObjectChooserEditlet())

#####################################################################
